package com.popcap.widgets;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.popcap.util.ArrayMap;
import com.popcap.util.GameDescriptorHandler;

public class WidgetsConfiguration
{
	public static final char NULL_CHAR = (char)((1<< 16) -1);
	private String m_WidgetDataString;
	private String m_WidgetOffsetString;
	private String m_ScreenDataString;
	private String m_ScreenOffsetString;
	private List<Constant> m_Constants = new ArrayList<Constant>();
	private Map<String, Item>  m_Widgets = new HashMap<String, Item>();
	private Map<String, Item> m_Screens = new HashMap<String, Item>();
	private List<Method> m_WidgetMethods = new ArrayList<Method>();
	private List<Method> m_ScreenMethods = new ArrayList<Method>();
	private Map<String, Type> m_WidgetTypes = new HashMap<String, Type>();
	private Map<String, Type> m_ScreenTypes = new HashMap<String, Type>();
	private List<String> m_WidgetOptions = new ArrayList<String>();
	//Have to use ArrayMap until we rewrite GameDescriptorHandler. I'll kill the guy who wrote ArrayMap!
	private ArrayMap gameTexts;
	private ArrayMap gameImages;
	Map<String, Integer> gameFonts = new HashMap<String, Integer>();
	
	public WidgetsConfiguration(String configFile, ArrayMap gameTexts, ArrayMap gameImages, List<String> fontsList) 
	{
		
		int i = 0;
		for(String fontName : fontsList)
		{
			System.out.println(fontName+":"+i);
			this.gameFonts.put(fontName, i++);
		}
		this.gameTexts = gameTexts;
		this.gameImages = gameImages;
		
	    try 
	    {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder parser = factory.newDocumentBuilder();
			Document doc = parser.parse(configFile);
			NamedNodeMap rootAttributes = doc.getDocumentElement().getAttributes();
			m_WidgetOptions.add("focusable");
			m_WidgetOptions.add("dynamic");
			NodeList nodeList = doc.getElementsByTagName("constant");
			createConstants(nodeList);
			nodeList = doc.getElementsByTagName("method");
			createMethods(nodeList);
			nodeList = doc.getElementsByTagName("screen-type");
			createTypes(nodeList, true);
			nodeList = doc.getElementsByTagName("widget-type");
			createTypes(nodeList, false);
			nodeList = doc.getElementsByTagName("screen");
			createScreens(nodeList);
			
			Properties props = new Properties();
			props.setProperty("directive.set.null.allowed", "true");
			Velocity.init(props);
			VelocityContext context = new VelocityContext();
			Template template = Velocity.getTemplate(rootAttributes.getNamedItem("template").getNodeValue());
			context.put("config", this);
			BufferedWriter writer = new BufferedWriter(new FileWriter(rootAttributes.getNamedItem("destination").getNodeValue()));
			template.merge(context, writer);
			writer.close();
	    }
	    catch (SAXException e) 
	    {
	    	System.out.println(configFile + " is not well-formed.");
	    	e.printStackTrace();
	    }
	    catch (IOException e) 
	    { 
	    	System.out.println("Exception opening: "  + configFile); 
	      	e.printStackTrace();
	    }
	    catch (FactoryConfigurationError e) 
	    { 
	      System.out.println("Could not locate a JAXP factory class");
	      e.printStackTrace();
	    }
	    catch (ParserConfigurationException e) 
	    { 
	      System.out.println("Could not locate a JAXP parser");
	      e.printStackTrace();
	    }
	    catch(Exception e)
	    {
	    	System.out.println("Error parsing UI configuration:");
	    	e.printStackTrace();
	    }
	   
	  }

	/**
	 * 
	 * @param nodes
	 */
	void createConstants(NodeList nodes)
	{
		for(int i = 0; i < nodes.getLength();i++)
		{
			Node node = nodes.item(i);
			NamedNodeMap attr = node.getAttributes();
			String name = attr.getNamedItem("name").getNodeValue();
			String type = attr.getNamedItem("type").getNodeValue();
			String value = attr.getNamedItem("value").getNodeValue();
			if("font".equals(type))
			{
				Integer fontId = gameFonts.get(value);
				if(fontId != null)
				{
					value = fontId.toString();
				}
				else
				{
					throw new RuntimeException("Could not find font id for " + value + " at constant "+ name);
				}
				type = "int";
			}
			else if("image".equals(type))
			{
				int imageId = gameImages.indexOf(value);
				if(imageId >= 0)
				{
					value = String.valueOf(imageId);
				}
				else
				{
					throw new RuntimeException("Could not find image id for " + value + " at constant "+ name);
				}
				type = "int";

			}
			else if("text".equals(type))
			{
				int textId = gameTexts.indexOf(value);
				if(textId>= 0)
				{
					value = String.valueOf(textId);
				}
				else
				{
					throw new RuntimeException("Could not find text id for " + value + " at constant "+ name);
				}
				type = "int";

			}
			m_Constants.add(new Constant(type, name, value));
		}
	}
	
	/**
	 * 
	 * @param nodes
	 */
	void createMethods(NodeList nodes)
	{
		for(int i = 0; i < nodes.getLength();i++)
		{
			Node node = nodes.item(i);
			NamedNodeMap attr = node.getAttributes();
			String type = attr.getNamedItem("type").getNodeValue();
			String name = attr.getNamedItem("name").getNodeValue();
			String returnType = attr.getNamedItem("return").getNodeValue();
			Node nodeDefault = attr.getNamedItem("default");
			String defaultTarget = null;
			if(nodeDefault != null)
			{
				defaultTarget = nodeDefault.getNodeValue();
			}
			
			Method met = new Method(returnType, name, defaultTarget);
			NodeList children = node.getChildNodes();
			for(int j = 0; j < children.getLength();j++)
			{
				Node child = children.item(j);
				if("arg".equals(child.getNodeName()))
				{
					NamedNodeMap childAttr = child.getAttributes();
					met.addCustomArgument(childAttr.getNamedItem("type").getNodeValue(), childAttr.getNamedItem("name").getNodeValue());
				}
			}
			
			if(type.equals("screen"))
			{
				m_ScreenMethods.add(met);
			}
			else if(type.equals("widget"))
			{
				m_WidgetMethods.add(met);
			}	
		}
	}
	
	/**
	 * 
	 * @param nodes
	 */
	void createTypes(NodeList nodes, boolean bScreen)
	{
		for(int i = 0; i < nodes.getLength();i++)
		{
			Node node = nodes.item(i);
			NamedNodeMap attr = node.getAttributes();
			String name = attr.getNamedItem("name").getNodeValue();
			int id = 0;
			if(bScreen)
			{
				id = m_ScreenTypes.size();
			}
			else
			{
				id = m_WidgetTypes.size();
			}
			Type type = new Type(id, name);
			//options
			for(int j = 0;j < attr.getLength();j++ )
			{
				Node at = attr.item(j);
				if(!at.getNodeName().equals("name"))
				{
					type.setOption(at.getNodeName(), at.getNodeValue());
				}
			}
			
			NodeList children = node.getChildNodes();
			for(int j = 0; j < children.getLength();j++)
			{
				Node child = children.item(j);
				NamedNodeMap childAttr = child.getAttributes();
				if("call".equals(child.getNodeName()))
				{
					type.setMethod(childAttr.getNamedItem("method").getNodeValue(), childAttr.getNamedItem("target").getNodeValue());
				}
				else if("property".equals(child.getNodeName()))
				{
					Node defaultNode = childAttr.getNamedItem("default");
					String defaultValue = null;
					if(defaultNode != null)
					{
						defaultValue = defaultNode.getNodeValue().trim();
						if(defaultValue.length() == 0)
						{
							defaultValue = null;
						}
					}
					type.addProperty(childAttr.getNamedItem("name").getNodeValue(), childAttr.getNamedItem("type").getNodeValue(), defaultValue);
				}
			}
			
			if(bScreen)
			{
				m_ScreenTypes.put(name, type);
			}
			else
			{
				m_WidgetTypes.put(name, type);
			}	
		}
	}

	/**
	 * 
	 * @param nodes
	 */
	void createScreens(NodeList nodes)
	{
		StringBuffer screenDataBuffer = new StringBuffer(512);
		StringBuffer widgetDataBuffer = new StringBuffer(512);
		StringBuffer screenOffsetBuffer = new StringBuffer(256);
		StringBuffer widgetOffsetBuffer = new StringBuffer(256);
		int maxWidgets = 0;
		for(int i = 0; i < nodes.getLength();i++)
		{
			screenOffsetBuffer.append((char)screenDataBuffer.length());
			Node node = nodes.item(i);
			NamedNodeMap attr = node.getAttributes();
			Type type = m_ScreenTypes.get(attr.getNamedItem("type").getNodeValue());
			if(type == null)
			{
				throw new RuntimeException("Unknown screen type: " + attr.getNamedItem("type").getNodeValue());
			}
			else
			{
				screenDataBuffer.append((char)type.getId());
			}
			int id = m_Screens.size();
			Node at = attr.getNamedItem("id");
			
			String idName;
			if(at != null)
			{
				idName = toFormalName(at.getNodeValue());
				if(m_Screens.containsKey(idName))
				{
					throw new RuntimeException("Duplicated screen id: " + idName);
				}
			}
			else
			{
				String inx = "";
				int index = 0;
				do
				{
					idName = toFormalName(type.getName()+"_"+inx+id);
					index++;
					inx = index + "_";
				}
				while(m_Screens.containsKey(idName));
			}
			
			Item screen = new Item(id, idName, type);
			NodeList children = node.getChildNodes();
			for(int j = 0; j < children.getLength();j++)
			{
				Node child = children.item(j);
				NamedNodeMap childAttr = child.getAttributes();
				if("property".equals(child.getNodeName()))
				{
					screen.setProperty(childAttr.getNamedItem("name").getNodeValue(), childAttr.getNamedItem("value").getNodeValue());
				}
				else if("call".equals(child.getNodeName()))
				{
					screen.setMethod(childAttr.getNamedItem("method").getNodeValue(), childAttr.getNamedItem("target").getNodeValue());
				}
				else if("widget".equals(child.getNodeName()))
				{
					Item widget = createWidget(child, screen);
					widgetOffsetBuffer.append((char)widgetDataBuffer.length());
					Map<String, String> widgetProps = widget.getProperties();
					Type wType = widget.getType();
					List<Property> typeProps = wType.getProperties();
					
					widgetDataBuffer.append((char)wType.getId());
					for(Property prop:typeProps)
					{
						System.out.println("Widget:"+prop);
						widgetDataBuffer.append(propertyValue(prop, widgetProps));
					}
					screen.addChild(widget);
				}
			}
			List<Item> widgets = screen.getChildren();
			int widgCount = (char)widgets.size();
			System.out.println("Widgets count: " + widgCount);
			screenDataBuffer.append((char)widgCount);
			if(widgCount > maxWidgets)
			{
				maxWidgets = widgCount;
			}
			for(Item widget : widgets)
			{
				screenDataBuffer.append((char)widget.getId());
			}
			Map<String, String> screenProps = screen.getProperties();
			List<Property> typeProps = type.getProperties();
			screenDataBuffer.append((char)typeProps.size());
			for(Property prop:typeProps)
			{
				System.out.println("Screen:"+prop);
				screenDataBuffer.append(propertyValue(prop, screenProps));
			}
			m_Screens.put(idName, screen);	
		}
		

		m_Constants.add(new Constant("String", "SCREEN_DATA", screenDataBuffer.toString()));
		m_Constants.add(new Constant("String", "SCREEN_OFFSET", screenOffsetBuffer.toString()));
		m_Constants.add(new Constant("String", "WIDGET_DATA", widgetDataBuffer.toString()));
		m_Constants.add(new Constant("String", "WIDGET_OFFSET", widgetOffsetBuffer.toString()));
		m_Constants.add(new Constant("int", "MAX_WIDGETS_PER_SCREEN",String.valueOf(maxWidgets)));
	}
	
	public static String toHexChars(String str)
	{
		StringBuffer result = new StringBuffer(512);
		boolean plus = false;
		for(int i = 0;i < str.length();i++ )
		{
			if(plus)
			{
				result.append("\n + ");
			}
			else
			{
				plus = true;
			}
			result.append("\"").append(GameDescriptorHandler.idToHexChar(str.charAt(i))).append("\"");
		}
		return result.toString();
	}
	
	private Item createWidget(Node node, Item parent)
	{
		NamedNodeMap attr = node.getAttributes();
		int id = m_Widgets.size();
		
		Type type = m_WidgetTypes.get(attr.getNamedItem("type").getNodeValue());
		if(type == null)
		{
			throw new RuntimeException("Unknown widget type: " + attr.getNamedItem("type").getNodeValue());
		}
		
		Node at = attr.getNamedItem("id");
		String idName;
		if(at != null)
		{
			idName = toFormalName(at.getNodeValue());
			if(m_Widgets.containsKey(idName))
			{
				throw new RuntimeException("Duplicated widget id: " + idName);
			}
		}
		else
		{
			String inx = "";
			int index = 0;
			do
			{
				idName = toFormalName(parent.getName()+"_"+ type.getName() + "_"+inx+id);
				index++;
				inx = index + "_";
			}
			while(m_Widgets.containsKey(idName));
		}
		Item widget = new Item(id, idName, type);		
		NodeList children = node.getChildNodes();
		for(int j = 0; j < children.getLength();j++)
		{
			Node child = children.item(j);
			NamedNodeMap childAttr = child.getAttributes();
			if("property".equals(child.getNodeName()))
			{
				widget.setProperty(childAttr.getNamedItem("name").getNodeValue(), childAttr.getNamedItem("value").getNodeValue());
			}
			else if("call".equals(child.getNodeName()))
			{
				widget.setMethod(childAttr.getNamedItem("method").getNodeValue(), childAttr.getNamedItem("target").getNodeValue());
			}
			
		}
		m_Widgets.put(idName, widget);
		return widget;
	}
	
	private char propertyValue(Property prop, Map<String, String> definedProps)
	{
		String type = prop.getType();
		String value = definedProps.get(prop.getName());
		System.out.println(type+"/"+value);
		if(value == null || value.trim().length() == 0)
		{
			value = prop.getDefaultValue();
		}
		if(value == null || value.trim().length() == 0)
		{
			return NULL_CHAR;
		}
		else
		{
			if("text".equals(type))
			{
				int id = gameTexts.indexOf(value);

				if(id < 0)
				{
					throw new RuntimeException("Game text for property <" + prop.getName() + "> not found: " + value);
				}
				else
				{
					return (char)id;
				}
			}
			else if("image".equals(type))
			{
				int id = gameImages.indexOf(value);
				if(id < 0)
				{
					throw new RuntimeException("Game image for property <" + prop.getName() + "> not found: " + value);
				}
				else
				{
					return (char)id;
				}
			}
			else if("font".equals(type))
			{
				if(!gameFonts.containsKey(value))
				{
					throw new RuntimeException("Game font for property <" + prop.getName() + "> not found: " + value);
				}
				else
				{
					int id = gameFonts.get(value);
					return (char)id;
				}
			}
			else if("boolean".equals(type))
			{
				return (char)("true".equalsIgnoreCase(value)?1:0);
			}
			else
			{
				return (char)Integer.parseInt(value);
			}
		}
		
	}
	
	public List<Constant> getConstants()
	{
		return m_Constants;
	}


	public List<Item> getWidgets()
	{
		Collection<Item> items = m_Widgets.values();
		Item[] sorted = new Item[items.size()];
		for(Item item:items)
		{
			sorted[item.getId()] = item;
		}
		return Arrays.asList(sorted);
	}


	public List<Item> getScreens()
	{
		Collection<Item> items = m_Screens.values();
		Item[] sorted = new Item[items.size()];
		for(Item item:items)
		{
			sorted[item.getId()] = item;
		}
		return Arrays.asList(sorted);
	}

	public List<Method> getWidgetMethods()
	{
		return m_WidgetMethods;
	}

	public List<Method> getScreenMethods()
	{
		return m_ScreenMethods;
	}

	public List<Type> getWidgetTypes()
	{
		Collection<Type> types = m_WidgetTypes.values();
		Type[] sorted = new Type[types.size()];
		for(Type type:types)
		{
			sorted[type.getId()] = type;
		}
		return Arrays.asList(sorted);
	}

	public List<Type> getScreenTypes()
	{
		Collection<Type> types = m_ScreenTypes.values();
		Type[] sorted = new Type[types.size()];
		for(Type type:types)
		{
			sorted[type.getId()] = type;
		}
		return Arrays.asList(sorted);
	}

	public List<String> getWidgetOptions()
	{
		return m_WidgetOptions;
	}
	
	/**
	 * Converts an ID name to a name suitable for a constant definition
	 * @param strName the name to convert
	 */
	public String toFormalName(String strName)
	{
		return strName.trim().replace('.', '_').replace(' ', '_').replace('-', '_').toUpperCase();
	}
	
	public String capitalize(String str)
	{
		if(str != null)
		{
			if(str.length() > 1)
			{
				return str.substring(0,1).toUpperCase() + str.substring(1);
			}
			else
			{
				return str.toUpperCase();
			}
		}
		else
		{
			return str;
		}
	}

}
